﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;

namespace Portables.Collections
{
   public class ObservableKeyedCollection<TKey, TItem> : IObservableCollection<TItem>, IList, INotifyPropertyChanged
   {
      #region protected fields

      protected readonly List<TItem> _items = new List<TItem>();
      protected readonly Dictionary<TKey, TItem> _itemsLookup;
      protected readonly IEqualityComparer<TKey> _keyComparer;
      protected readonly Func<TItem, TKey> _keyFactory;
      protected readonly IComparer<TItem> _sortComparer;

      protected readonly object _syncRoot = new object();

      #endregion

      #region constructor

      public ObservableKeyedCollection(Func<TItem, TKey> keyFactory, IEqualityComparer<TKey> keyComparer = null, IComparer<TItem> sortComparer = null)
      {
         if(keyFactory == null) throw new ArgumentNullException("keyFactory");
         _keyFactory = keyFactory;
         _keyComparer = keyComparer ?? EqualityComparer<TKey>.Default;
         _itemsLookup = new Dictionary<TKey, TItem>(keyComparer);
         _sortComparer = sortComparer;
      }

      #endregion

      #region public properties

      public bool IsReadOnly
      {
         get { return false; }
      }

      public int Count
      {
         get { return _items.Count; }
      }

      public IEqualityComparer<TKey> KeyComparer
      {
         get { return _keyComparer; }
      }

      public Func<TItem, TKey> KeyFactory
      {
         get { return _keyFactory; }
      }

      public IComparer<TItem> SortComparer
      {
         get { return _sortComparer; }
      }

      #endregion

      #region public methods

      public void Add(TItem item)
      {
         if(_sortComparer == null)
         {
            TKey key = _keyFactory(item);
            _itemsLookup.Add(key, item);
            _items.Add(item);

            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _items.Count - 1));
         }
         else
         {
            InsertSorted(item);
         }
      }

      public void AddOrUpdate(TItem item)
      {
         TKey key = _keyFactory(item);
         TItem existing;
         if(_itemsLookup.TryGetValue(key, out existing))
         {
            SetItem(_items.IndexOf(existing), item);
         }
         else
         {
            Add(item);
         }
      }

      public void Clear()
      {
         _items.Clear();
         _itemsLookup.Clear();

         OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
      }

      public bool Contains(TItem item)
      {
         return _items.Contains(item);
      }

      public bool ContainsKey(TKey key)
      {
         return _itemsLookup.ContainsKey(key);
      }

      public void CopyTo(TItem[] array, int arrayIndex)
      {
         _items.CopyTo(array, arrayIndex);
      }

      public IEnumerator<TItem> GetEnumerator()
      {
         return _items.GetEnumerator();
      }

      IEnumerator IEnumerable.GetEnumerator()
      {
         return _items.GetEnumerator();
      }

      public int IndexOf(TItem item)
      {
         return _items.IndexOf(item);
      }

      public void Insert(int index, TItem item)
      {
         if(_sortComparer != null) throw new InvalidOperationException("Insert not permitted while SortComparer is not null.");
         TKey key = _keyFactory(item);
         if(_itemsLookup.ContainsKey(key)) throw new InvalidOperationException("Duplicate key");
         _items.Insert(index, item);
         _itemsLookup.Add(key, item);

         OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
      }

      public bool Remove(TItem item)
      {
         TKey key = _keyFactory(item);
         if(_itemsLookup.Remove(key))
         {
            int index = _items.IndexOf(item);
            _items.Remove(item);

            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
            return true;
         }
         return false;
      }

      public bool RemoveKey(TKey key)
      {
         TItem item;
         if(_itemsLookup.TryGetValue(key, out item))
         {
            int index = _items.IndexOf(item);
            _items.Remove(item);
            _itemsLookup.Remove(key);

            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
            return true;
         }
         return false;
      }

      public bool TryGetValue(TKey key, out TItem value)
      {
         return _itemsLookup.TryGetValue(key, out value);
      }

      #endregion

      #region protected methods

      protected void InsertSorted(TItem item)
      {
         TKey key = _keyFactory(item);
         if(_itemsLookup.ContainsKey(key)) throw new InvalidOperationException("Duplicate key");

         int index = _items.BinarySearch(item, _sortComparer);
         if(index < 0)
         {
            index = Math.Abs(index) - 1;
         }
         _items.Insert(index, item);
         _itemsLookup.Add(key, item);

         OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
      }

      protected void SetItem(int index, TItem value)
      {
         if(Equals(value, null)) throw new ArgumentNullException("value");
         if(index >= _items.Count) throw new IndexOutOfRangeException();

         TItem old = _items[index];
         TKey oldKey = _keyFactory(old);
         TKey newKey = _keyFactory(value);

         if(!_keyComparer.Equals(oldKey, newKey) && _itemsLookup.ContainsKey(newKey))
         {
            throw new InvalidOperationException("Duplicate key");
         }

         if(_sortComparer != null && Math.Abs(_items.BinarySearch(value, _sortComparer)) != index)
         {
            throw new InvalidOperationException("Unable to insert element at specified index because SortComparer is not null and the new item belongs at a different sort position.");
         }

         _itemsLookup.Remove(oldKey);
         _itemsLookup.Add(newKey, value);
         _items[index] = value;

         OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, old, index));
      }

      protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
      {
         CollectionChanged(this, args);

         if(args.Action != NotifyCollectionChangedAction.Replace)
         {
            OnPropertyChanged(new PropertyChangedEventArgs("Count"));
         }
      }

      protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
      {
         PropertyChanged(this, args);
      }

      #endregion

      #region public events

      public event NotifyCollectionChangedEventHandler CollectionChanged = delegate { };
      public event PropertyChangedEventHandler PropertyChanged = delegate { };

      #endregion

      #region IList<T> explicit members

      object IList.this[int index]
      {
         get { return _items[index]; }
         set { SetItem(index, (TItem)value); }
      }

      int ICollection.Count
      {
         get { return _items.Count; }
      }

      bool IList.IsFixedSize
      {
         get { return false; }
      }

      bool IList.IsReadOnly
      {
         get { return false; }
      }

      bool ICollection.IsSynchronized
      {
         get { return false; }
      }

      object ICollection.SyncRoot
      {
         get { return _syncRoot; }
      }

      int IList.Add(object value)
      {
         Add((TItem)value);
         return IndexOf((TItem)value);
      }

      void IList.Clear()
      {
         Clear();
      }

      bool IList.Contains(object value)
      {
         return _items.Contains((TItem)value);
      }

      void ICollection.CopyTo(Array array, int index)
      {
         _items.Cast<object>().ToArray().CopyTo(array, index);
      }

      int IList.IndexOf(object value)
      {
         return _items.IndexOf((TItem)value);
      }

      void IList.Insert(int index, object value)
      {
         Insert(index, (TItem)value);
      }

      void IList.Remove(object value)
      {
         _items.Remove((TItem)value);
      }

      void IList.RemoveAt(int index)
      {
         _items.RemoveAt(index);
      }

      #endregion
   }
}